﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Noris.Schedule.Support.Parser;
using Noris.Schedule.Support.Services;
using Noris.Schedule.Support;

namespace Noris.Tools.FrxEditor.SqlCommands
{
    #region class SqlCommand : třída, která zpracuje text SQL commandu na řadu objektů, které obsahují zadané příkazy.
    /// <summary>
    /// SqlCommand : třída, která zpracuje text SQL commandu na řadu objektů, které obsahují zadané příkazy.
    /// </summary>
    public class SqlCommand
    {
        /// <summary>
        /// Zpracuje daný SQL text na jednotlivé příkazy, a ty vrátí,
        /// </summary>
        /// <param name="sqlText"></param>
        /// <returns></returns>
        public static IEnumerable<ISqlCommand> Parse(string sqlText)
        {
            SqlStream sqlStream = SqlStream.ParseSqlCommand(sqlText);
            return Parse(sqlStream);
        }
        /// <summary>
        /// Zpracuje daný SQL stream na jednotlivé příkazy, a ty vrátí.
        /// </summary>
        /// <param name="sqlStream"></param>
        /// <returns></returns>
        public static IEnumerable<ISqlCommand> Parse(SqlStream sqlStream)
        {
            List<ISqlCommand> result = new List<ISqlCommand>();
            List<ISqlCommand> allCommands = new List<ISqlCommand>(Steward.GetServiceObjectList<ISqlCommand>(true));

            while (!sqlStream.EndOfStream)
            {
                int pointer = sqlStream.Pointer;

                ISqlCommand command = FindSqlCommand(sqlStream, allCommands);
                if (command != null)
                {
                    ISqlCommand sqlCommand = Steward.GetServiceObject<ISqlCommand>(command.GetType(), false);
                    sqlCommand.LoadFrom(sqlStream);
                    if (sqlStream.Pointer == pointer)
                        throw new ApplicationException("SqlCommand parser error: command «" + command.GetType().Name + "» did not process a word.");
                    result.Add(sqlCommand);
                }
                else
                    sqlStream.ReadValue();
            }

            return result;
        }
        /// <summary>
        /// Najde v seznamu všech existujících SQL příkazů ten, který bude zpracovávat aktulání příkaz ze SqlStreamu.
        /// Pokud se nikdo nepřihlásí, vrátí null.
        /// </summary>
        /// <param name="sqlStream"></param>
        /// <param name="allCommands"></param>
        /// <returns></returns>
        private static ISqlCommand FindSqlCommand(SqlStream sqlStream, List<ISqlCommand> allCommands)
        {
            foreach (ISqlCommand commands in allCommands)
            {
                if (commands.IsMyPart(sqlStream))
                    return commands;
            }
            return null;
        }
        /// <summary>
        /// Konstanta pro typ segmentu "CASE"
        /// </summary>
        public const string EXPR_CASE = "CASE";
    }
    #endregion
    #region class SqlStream : stream položek (ParserSegmentValue) nad parsovaným SQL textem. Řeší čtení položek, ignorování blank a komentářů, řeší Preview (čtení bez posunu).
    /// <summary>
    /// SqlStream : stream položek (ParserSegmentValue) nad parsovaným SQL textem.
    /// Řeší čtení položek, ignorování blank a komentářů, řeší Preview (čtení bez posunu).
    /// </summary>
    public class SqlStream
    {
        #region Konstrukce streamu, data
        public static SqlStream ParseSqlCommand(string sqlCommand)
        {
            List<ParserSegment> segments = Parser.ParseString(sqlCommand, ParserDefaultSetting.MsSql);
            return new SqlStream(segments);
        }
        public SqlStream(List<ParserSegment> segments)
        {
            this.Values = new List<ParserSegmentValue>();
            this.FillValues(segments);
            // this.Segment = segments[0];
            this.Pointer = 0;
        }
        public override string ToString()
        {
            string text = "";
            for (int i = this.Pointer; i < this.Pointer + 12; i++)
            {
                if (i >= this.Values.Count) break;
                text += this.Values[i].Text + " ";
            }
            return text.Trim();
        }
        /// <summary>
        /// Položky SQL streamu.
        /// Jde o hodnoty (Value) z dodaného segmentu = první úroveň SQL textu.
        /// Tzn. případné vnořené příkazy nebo výrazy (v závorkách, apostrofech, atd) jsou zde uloženy v jedné položce Values, která je typu Segment a obsahuje svůj segment a jeho Values (rekurzivně...).
        /// V tomto seznamu nejsou vloženy hodnoty typu Blank anebo Komentář.
        /// V tomto seznamu jsou v textových prvcích, které nejsou KEYWORD, v případě potřeby sloučené texty oddělené mezerami a tečkami 
        /// (řeší se tím speciální možnost SQL psát názvy prvků například: "lcs . [product_order] . [column] ")
        /// </summary>
        public List<ParserSegmentValue> Values { get; private set; }
        /// <summary>
        /// Ukazatel na aktuální segment. Úvodní hodnota = 0.
        /// </summary>
        public int Pointer { get; set; }
        #endregion
        #region Příprava streamu = načítání Values
        /// <summary>
        /// Načte do sebe všechny hodnoty z dodaných segmentů
        /// </summary>
        /// <param name="segments"></param>
        protected void FillValues(IEnumerable<ParserSegment> segments)
        {
            foreach (ParserSegment segment in segments)
                this.FillValues(segment);
        }
        /// <summary>
        /// Načte do sebe všechny hodnoty z dodaného segmentu
        /// </summary>
        /// <param name="segment"></param>
        protected void FillValues(ParserSegment segment)
        {
            ParserSegmentValue mergeText = null;                            // Value, do které mergujeme texty a tečky
            FillValueChainState endState = FillValueChainState.None;        // Aktuální (=předchozí) stav, může mít hodnoty pouze: None, IsText a IsDot. Jiné hodnoty nenabývá.  Hodnotu IsDot má i tehdy, když byl naposledy Text, který končil tečkou (například "lcs.")
            foreach (ParserSegmentValue value in segment.Values)
            {
                string content;
                FillValueChainState currState = DetectChainState(value);     // Čemu odpovídá aktuální prvek. Hodnotu IsDot má tehdy, když tečkou začíná (nebo to je tečka).
                FillValueChainState nextState;
                switch (currState)
                {
                    case FillValueChainState.None:
                    case FillValueChainState.Ignore:
                        break;

                    case FillValueChainState.IsText:
                        // Nyní je text, který nezačíná tečkou (pozor: může jít i o segment SYSNAME, který ale nemá Content!) :
                        content = value.Text.Trim();
                        nextState = (content.EndsWith(".") ? FillValueChainState.IsDot : FillValueChainState.IsText);
                        switch (endState)
                        {   // lastState může být pouze None, IsText a IsDot:
                            case FillValueChainState.None:
                                // Dosud nebylo nic, teď je text:
                                this.Values.Add(value);
                                mergeText = value;
                                break;
                            case FillValueChainState.IsText:
                                // Dosud byl text (nekončil tečkou), nyní je text (nezačíná tečkou) = dva oddělené texty:
                                this.Values.Add(value);
                                mergeText = value;
                                break;
                            case FillValueChainState.IsDot:
                                // Dosud byl text končící tečkou, nyní je text (nezačíná tečkou):
                                mergeText.AddContent(content, true);
                                break;
                        }
                        endState = nextState;
                        break;

                    case FillValueChainState.IsDot:
                        // Nyní je to text, který začíná tečkou (tím pádem to nebude segment SYSNAME, ten tečkou nikdy nezačíná):
                        content = value.Content.Trim();
                        nextState = (content.EndsWith(".") ? FillValueChainState.IsDot : FillValueChainState.IsText);
                        switch (endState)
                        {   // lastState může být pouze None, IsText a IsDot:
                            case FillValueChainState.None:
                                // Dosud nebylo nic, teď je tečka [a text]:
                                this.Values.Add(value);
                                mergeText = value;
                                break;
                            case FillValueChainState.IsText:
                                // Dosud byl text (nekončil tečkou), teď je tečka [a text]:
                                mergeText.AddContent(content, true);
                                break;
                            case FillValueChainState.IsDot:
                                // Dosud byl text končící tečkou, nyní je text (nezačíná tečkou):
                                mergeText.AddContent(content, true);
                                break;
                        }
                        endState = nextState;
                        break;

                    case FillValueChainState.Other:
                        // Nyní je něco jiného:
                        this.Values.Add(value);
                        mergeText = null;
                        endState = FillValueChainState.None;
                        break;

                }
            }
        }
        /// <summary>
        /// Určí, zda daný prvek je možno řetězit, a případně jakou roli v řetězu má (nic / text / tečka).
        /// Hodnotu IsDot vrací pro SimpleText, který začíná tečkou (nebo je to pouze tečka).
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        protected static FillValueChainState DetectChainState(ParserSegmentValue value)
        {
            if (value == null)
                return FillValueChainState.None;

            if (value.ValueType == ParserSegmentValueType.Blank)
                return FillValueChainState.Ignore;
            if (value.ValueType == ParserSegmentValueType.InnerSegment && value.HasInnerSegment && (value.InnerSegment.SegmentName == ParserDefaultSetting.SQL_COMMENTBLOCK || value.InnerSegment.SegmentName == ParserDefaultSetting.SQL_COMMENTLINE))
                return FillValueChainState.Ignore;
            if (value.ValueType == ParserSegmentValueType.Text && value.ValueName != Parser.VALUENAME_KEYWORD && value.HasContent)
                return ((value.Content.Trim().StartsWith(".")) ? FillValueChainState.IsDot : FillValueChainState.IsText);
            else if (value.HasInnerSegment && value.InnerSegment.SegmentName == ParserDefaultSetting.SQL_SYSNAME)
                return FillValueChainState.IsText;
            return FillValueChainState.Other;
        }
        /// <summary>
        /// Stav hodnoty z hlediska možného slučování textů a teček mezi texty
        /// </summary>
        protected enum FillValueChainState
        {
            None,
            Ignore,
            IsText,
            IsDot,
            Other
        }
        /// <summary>
        /// Vrací true, pokud daná hodnota (ParserSegmentValue) je Blank
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static bool IsBlank(ParserSegmentValue value)
        {
            if (value == null) return false;
            return (value.ValueType == ParserSegmentValueType.Blank);
        }
        /// <summary>
        /// Vrací true, pokud daná hodnota (ParserSegmentValue) je komentář
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static bool IsComment(ParserSegmentValue value)
        {
            if (value == null) return false;
            return (value.ValueType == ParserSegmentValueType.InnerSegment && value.HasInnerSegment &&
                      (value.InnerSegment.SegmentName == ParserDefaultSetting.SQL_COMMENTBLOCK ||
                       value.InnerSegment.SegmentName == ParserDefaultSetting.SQL_COMMENTLINE));
        }
        /// <summary>
        /// Vrací true, pokud daná hodnota (ParserSegmentValue) je prostý text, nikoli KEYWORD
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static bool IsSimpleText(ParserSegmentValue value)
        {
            if (value == null) return false;
            return (value.ValueType == ParserSegmentValueType.Text && value.ValueName != Parser.VALUENAME_KEYWORD && value.HasContent);
        }
        #endregion
        #region Detekce slov ve streamu, přeskočení slov ve streamu
        /// <summary>
        /// Metoda zjistí, zda na aktuální pozici streamu se nachází některé z předaných klíčových slov/sousloví.
        /// Metoda neposouvá stream, používá Preview.
        /// Zadání klíčových slov: jeden string. Může obsahovat více hledaných slov/sousloví, odělených čárkou.
        /// Jedna položka může mít více slov, oddělené mezerou.
        /// Jedno slovo může být nahrazeno otazníkem = na jeho místě může být libovolná položka (i vnořený segment, v závorce).
        /// Typické zadání může být tedy: string found = stream.BeginWithKeyword("INNER ? JOIN, LEFT JOIN, RIGHT JOIN").
        /// Metoda vrací příznak nalezení slova (true / false).
        /// </summary>
        /// <param name="keyWords"></param>
        /// <returns></returns>
        public bool StartsWithKeyword(string keyWords)
        {
            string keyWord = FindInitialKeyword(keyWords);
            return (keyWord != null);
        }
        /// <summary>
        /// Metoda zjistí, zda na aktuální pozici streamu se nachází některé z předaných klíčových slov/sousloví.
        /// Metoda neposouvá stream, používá Preview.
        /// Zadání klíčových slov: jeden string. Může obsahovat více hledaných slov/sousloví, odělených čárkou.
        /// Jedna položka může mít více slov, oddělené mezerou.
        /// Jedno slovo může být nahrazeno otazníkem = na jeho místě může být libovolná položka (i vnořený segment, v závorce).
        /// Typické zadání může být tedy: string found = stream.BeginWithKeyword("INNER ? JOIN, LEFT JOIN, RIGHT JOIN").
        /// Metoda vrací příznak nalezení slova (true / false), a nalezené klíčové slovo ukládá do out parametru keyWord.
        /// </summary>
        /// <param name="keywords">Přípustná slova</param>
        /// <param name="keyword">Nalezené slovo nebo null</param>
        /// <returns></returns>
        public bool StartsWithKeyword(string keywords, out string keyword)
        {
            keyword = FindInitialKeyword(keywords);
            return (keyword != null);
        }
        /// <summary>
        /// Metoda zjistí, zda na aktuální pozici streamu se nachází některé z předaných klíčových slov/sousloví.
        /// Metoda neposouvá stream, používá Preview.
        /// Zadání klíčových slov: jeden string. Může obsahovat více hledaných slov/sousloví, odělených čárkou.
        /// Jedna položka může mít více slov, oddělené mezerou.
        /// Jedno slovo může být nahrazeno otazníkem = na jeho místě může být libovolná položka (i vnořený segment, v závorce).
        /// Typické zadání může být tedy: string found = stream.BeginWithKeyword("INNER ? JOIN, LEFT JOIN, RIGHT JOIN").
        /// Metoda vrací klíčové slovo, které nalezla, nebo vrací null když nenalezla nic.
        /// </summary>
        /// <param name="keywords"></param>
        /// <returns></returns>
        public string FindInitialKeyword(string keywords)
        {
            if (keywords == null) return null;

            // Rozeberu zadaný text na prvky:
            List<KeywordPhrase> phrases = new List<KeywordPhrase>();      // Jedna položka Listu = jedno sousloví (obsahuje pole oddělených slov { "LEFT", "OUTER" }).
            string[] separator = new string[] { KEYWORDS_SEPARATOR };
            string[] keyws = keywords.Split(separator, StringSplitOptions.RemoveEmptyEntries);
            foreach (string phrase in keyws)
                phrases.Add(new KeywordPhrase(phrase));
            int phraseCount = phrases.Count;
            if (phraseCount == 0) return null;
            if (phraseCount > 1)
                phrases.Sort(KeywordPhrase.CompareByCountDesc);
            int maxWordCount = phrases[0].WordCount;                // Nejvyšší počet slov ze všech sousloví je v první položce (jsou setříděné podle WordCount DESC)

            // Načtu si potřebný počet Value, načítám metodou PeekValues() = v Preview modu:
            List<ParserSegmentValue> streamValues = this.PeekValues(maxWordCount);

            // Projdu si sousloví od nejdelšího, a prověřím zda aktuální stream obsahuje jeho požadovaná slova:
            foreach (KeywordPhrase phrase in phrases)
            {
                if (phrase.IsEquivalent(streamValues))
                    return phrase.Phrase;
            }
            return null;
        }
        /// <summary>
        /// Metoda přeskočí ve streamu přes zadané klíčové slovo (které může být víceslovné, například "ORDER BY") a nechá stream stát na první Value za tímto slovem. Vrací true.
        /// Pokud stream obsahuje něco jiného, nechá stream stát na prvním nesouhlasném prvku a vrátí false.
        /// </summary>
        /// <param name="keyword">Klíčové slovo, které se má přeskočit. Může být víceslovné, například "ORDER BY".</param>
        /// <returns></returns>
        public bool SkipKeyword(string keyword)
        {
            List<ParserSegmentValue> values;
            return this.SkipKeyword(keyword, out values);
        }
        /// <summary>
        /// Metoda přeskočí ve streamu přes zadané klíčové slovo (které může být víceslovné, například "ORDER BY") a nechá stream stát na první Value za tímto slovem. Vrací true.
        /// Pokud stream obsahuje něco jiného, nechá stream stát na prvním nesouhlasném prvku a vrátí false.
        /// </summary>
        /// <param name="keyword">Klíčové slovo, které se má přeskočit. Může být víceslovné, například "ORDER BY".</param>
        /// <param name="values">Výstup Values, které byly nalezeny a přeskočeny</param>
        /// <returns></returns>
        public bool SkipKeyword(string keyword, out List<ParserSegmentValue> values)
        {
            values = null;
            if (String.IsNullOrEmpty(keyword)) return false;
            values = new List<ParserSegmentValue>();
            if (keyword == "?") return true;
            KeywordPhrase phrase = new KeywordPhrase(keyword);
            for (int i = 0; i < phrase.WordCount; i++)
            {
                if (this.EndOfStream) return false;
                ParserSegmentValue value = this.PeekValue();
                if (!phrase.IsEquivalent(value, i)) return false;
                values.Add(value);
                this.SkipValue();
            }
            return true;
        }
        /// <summary>
        /// Oddělovač klíčových sousloví.
        /// Jedno klíčové slovo se může skládat z více slov, například "LEFT OUTER JOIN".
        /// Do vyhledávacích metod lze předat jejich sadu, kde jednotlivá sousloví jsou oddělena tímto separátorem (středník). 
        /// Například: "LEFT OUTER JOIN; RIGHT JOIN; LEFT ? JOIN" a pod.
        /// Středník (v této verzi). Používat tuto konstantu, prosím.
        /// </summary>
        public const string KEYWORDS_SEPARATOR = ";";
        #endregion
        #region Čtení ze streamu
        /// <summary>
        /// Metoda najde v aktuálním streamu nejbližší významný prvek a vrátí jej.
        /// Metoda vždy posouvá ukazatel na následující prvek (ale nijak neřeší, co je ten další prvek zač).
        /// Metoda nevrací prvky typu Blank, a nevrací ani segmenty typu Komentář.
        /// Vrací prvky (Value), které obsahují vnitřní segmenty = typicky vnitřní závorky, 
        /// anebo prvky čistého textu.
        /// Metoda může vrátit null, pokud dojde na konec streamu a nic nenajde.
        /// </summary>
        /// <returns></returns>
        public ParserSegmentValue ReadValue()
        {
            return (this._GetValue(true));
        }
        /// <summary>
        /// Metoda najde v aktuálním streamu nejbližší významný prvek a vrátí jej.
        /// Tato metoda NEPOSOUVÁ ukazatel, na rozdíl od metody ReadValue() (která ukazatel posune).
        /// Metoda nevrací prvky typu Blank, a nevrací ani segmenty typu Komentář.
        /// Vrací prvky (Value), které obsahují vnitřní segmenty = typicky vnitřní závorky, 
        /// anebo prvky čistého textu.
        /// Metoda může vrátit null, pokud dojde na konec streamu a nic nenajde.
        /// </summary>
        /// <returns></returns>
        public ParserSegmentValue PeekValue()
        {
            return (this._GetValue(false));
        }
        /// <summary>
        /// Přejde na další nejbližší významný prvek, nevrací jej, pouze posouvá pointer.
        /// Vrací true, pokud se další položka našla.
        /// </summary>
        /// <returns></returns>
        public bool SkipValue()
        { 
            ParserSegmentValue value = this._GetValue(true);
            return (value != null);
        }
        /// <summary>
        /// Přeskočí zadaný počet prvků a posune se na následující položku, pouze posouvá pointer.
        /// Vrací true, pokud se podařilo přejít na požadovanou položku, nebo false když žádná další není.
        /// </summary>
        /// <returns></returns>
        public bool SkipValues(int count)
        {
            for (int c = 0; c < count; c++)
            {
                ParserSegmentValue value = this._GetValue(true);
                if (value == null)
                    return false;
            }
            return true;
        }
        /// <summary>
        /// Metoda najde v aktuálním streamu nejbližší významné prvky v požadovaném počtu a vrátí je.
        /// Tato metoda POSOUVÁ ukazatel, na rozdíl od metody PeekValues() (která ukazatel neposune).
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
        public List<ParserSegmentValue> ReadValues(int count)
        {
            return this._GetValues(count);
        }
        /// <summary>
        /// Metoda najde v aktuálním streamu nejbližší významné prvky v požadovaném počtu a vrátí je.
        /// Tato metoda NEPOSOUVÁ ukazatel, na rozdíl od metody ReadValues() (která ukazatel posune).
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
        public List<ParserSegmentValue> PeekValues(int count)
        {
            using (this.Preview())
            {
                return this._GetValues(count);
            }
        }
        /// <summary>
        /// Metoda najde v aktuálním streamu nejbližší významné prvky v požadovaném počtu a vrátí je.
        /// Tato metoda POSOUVÁ ukazatel. Volající metoda může volání obalit do Preview modu a ukazatel tak po skončení metody vrátit.
        /// </summary>
        /// <param name="count">Požadovaný počet prvků. Může být vráceno méně, když nebudou nalezeny další.</param>
        /// <returns></returns>
        private List<ParserSegmentValue> _GetValues(int count)
        {
            List<ParserSegmentValue> result = new List<ParserSegmentValue>();
            for (int c = 0; c < count; c++)
            {
                ParserSegmentValue value = this._GetValue(true);
                if (value == null)
                    break;
                result.Add(value);
            }
            return result;
        }
        /// <summary>
        /// Metoda najde a vrátí nejbližší významný prvek ve streamu.
        /// Volitelně aktualizuje Pointer - podle parametru setPointer: true posouvá pointer, false neposouvá.
        /// </summary>
        /// <returns></returns>
        private ParserSegmentValue _GetValue(bool setPointer)
        {
            if (this.EndOfStream) return null;

            // Protože Stream obsahuje jen významově důležité prvky (neobsahuje Blank ani Comment), mohu zde vzít bezprostředně nejbližší prvek:
            ParserSegmentValue result = this.Values[this.Pointer];

            if (setPointer)
                this.Pointer++;

            return result;
        }
        /// <summary>
        /// Příznak, že tento stream už je za koncem vstupního textu.
        /// Porovnává aktuální pointer this.Pointer proti seznamu this.Values.
        /// </summary>
        public bool EndOfStream { get { return this.IsEndOfStream(this.Pointer); } }
        /// <summary>
        /// Příznak, že tento stream už je za koncem vstupního textu.
        /// Porovnává aktuální pointer this.Pointer proti seznamu this.Values.
        /// </summary>
        public bool IsEndOfStream(int pointer)
        {
            return (pointer < 0 || pointer >= this.Values.Count);
        }
        #endregion
        #region Preview only
        /// <summary>
        /// Zahajuje blok příkazů using() { ... }, kdy uvnitř bloku lze libovolně pohybovat streamem, a na konci bloku se stream vrátí do výchozí pozice.
        /// Typické využití je v metodách IsMyPart(stream).
        /// Optimální použití je: using (stream.Preview() { testy streamu; } return; .
        /// </summary>
        /// <returns></returns>
        public IDisposable Preview()
        {
            return new _SqlStreamPreview(this.Values, this.Pointer, this._PreviewEnd);
        }
        /// <summary>
        /// Na konci života objektu Preview: vrátí segment i pointer na hodnoty uložené při startu.
        /// </summary>
        /// <param name="values"></param>
        /// <param name="pointer"></param>
        private void _PreviewEnd(List<ParserSegmentValue> values, int pointer)
        {
            this.Values = values;
            this.Pointer = pointer;
        }
        /// <summary>
        /// IDisposable třída ukládající návratové hodnoty pro Preview streamu.
        /// </summary>
        private class _SqlStreamPreview : IDisposable
        {
            public _SqlStreamPreview(List<ParserSegmentValue> values, int pointer, Action<List<ParserSegmentValue>, int> onDispose)
            {
                this._Values = values;
                this._Pointer = pointer;
                this._OnDispose = onDispose;
            }
            private List<ParserSegmentValue> _Values;
            private int _Pointer;
            private Action<List<ParserSegmentValue>, int> _OnDispose;
            void IDisposable.Dispose()
            {
                this._OnDispose(this._Values, this._Pointer);
                this._Values = null;
            }
        }
        #endregion
        #region SubStream
        /// <summary>
        /// Vrátí podmnožinu tohoto streamu, v daném intervalu (first, count)
        /// </summary>
        /// <param name="first"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        public IEnumerable<ParserSegmentValue> SubPart(int first, int length)
        {
            return this.Values.GetRange(first, length);
        }
        /// <summary>
        /// Vrátí text z dodaných položek streamu
        /// </summary>
        /// <param name="values"></param>
        /// <returns></returns>
        public static string ToText(IEnumerable<ParserSegmentValue> values)
        {
            StringBuilder sb = new StringBuilder();
            foreach (ParserSegmentValue value in values)
                sb.Append(value.Text + " ");
            return sb.ToString().Trim();
        }
        #endregion
    }
    #endregion
    #region class SqlPart : abstraktní třída, která je vhodným předkem pro všechny SQL parts.
    /// <summary>
    /// SqlPart : abstraktní třída, která je vhodným předkem pro všechny SQL parts.
    /// Umožní potomkovi deklarovat jeho sady klíčových slov (kombinace slov), jimiž může / musí začínat (property this.KeyWords).
    /// Zajistí za něj detekci ISqlPart.IsMyPart() = prohledá seznam klíčových slov, a podle výsledku odpoví.
    /// </summary>
    public abstract class SqlPart : ISqlPart
    {
        #region Servisní metody
        #endregion
        #region Public property a metody
        /// <summary>
        /// Vizualizace
        /// </summary>
        /// <returns></returns>
        public override string ToString() { return this.Text; }
        /// <summary>
        /// Obsahuje textovou reprezentaci této SqlPart.
        /// Pokud je IsUsed = false, obsahuje prázdný string.
        /// Pokud je IsUsed = true, pak ve výchozím stavu obsahuje souhrn Values.
        /// Do této property lze vložit explicitní hodnotu, pak vrací ji.
        /// Pokud bude vloženo null, bude zase vracet výchozí hodnotu.
        /// </summary>
        public virtual string Text
        {
            get
            {
                if (!this.IsUsed) return "";
                if (this.TextExplicit != null) return this.TextExplicit;
                return SqlStream.ToText(this.Values);
            }
            set
            {
                this.TextExplicit = value;
                this.IsUsed = (value != null || this.StreamLength > 0);
            }
        }
        /// <summary>
        /// Explicitně vložený text
        /// </summary>
        protected string TextExplicit;
        /// <summary>
        /// Příznak, že tato část SQL příkazu byla nalezena a použita.
        /// Každá část SQL příkazu je načítána ze SQL streamu, ale ne každá část je v něm skutečně přítomna.
        /// </summary>
        public bool IsUsed { get; protected set; }
        /// <summary>
        /// Text reálně nalezeného klíčového slova.
        /// </summary>
        public string Keyword { get; protected set; }
        /// <summary>
        /// Text chyby nalezené v této části
        /// </summary>
        public string Error { get; protected set; }
        /// <summary>
        /// Položka segmentu, která nebyla očekávaná a na níž došlo k chybě (this.Error)
        /// </summary>
        public ParserSegmentValue ErrorAtValue { get; protected set; }
        /// <summary>
        /// Ukazatel na první položku streamu, která je načtena do této SqlPart.
        /// </summary>
        public int StreamPointer { get; protected set; }
        /// <summary>
        /// Počet prvků streamu načtených do této SqlPart.
        /// </summary>
        public int StreamLength { get; protected set; }
        /// <summary>
        /// Prvky streamu, které byly načteny do této SqlPart.
        /// </summary>
        public IEnumerable<ParserSegmentValue> Values { get { return (this.Stream == null ? null : this.Stream.SubPart(this.StreamPointer, this.StreamLength)); } }
        /// <summary>
        /// Stream, který se do této SqlPart načítal
        /// </summary>
        public SqlStream Stream { get; protected set; }
        #endregion
        #region Protected virtual property a metody
        /// <summary>
        /// Property vrací sadu klíčových slov, které mohou uvozovat tuto část.
        /// Může být více formulací, odděleny musí být čárkou nebo středníkem. (například "LEFT OUTER JOIN, RIGHT JOIN", atd).
        /// Jednotlivý prvek je jedna kombinace ("LEFT OUTER JOIN"). Jednotlivá slova jsou oddělena mezerou.
        /// Prvek může mít více kombinací klíčových slov.
        /// Je třeba na první pozice dávat delší texty, a na pozdější pozice slova kratší, protože se vyhledává první odpovídající slovo.
        /// Na místě klíčového slova může být OTAZNÍK, což značí "na tomto místě cokoliv". Například "LEFT OUTER ? JOIN" povoluje použití hintů (konkrétně: "LEFT OUTER HASH JOIN").
        /// </summary>
        protected virtual string Keywords { get { return null; } }
        /// <summary>
        /// Metoda je volána tehdy, když bylo zjištěno, že aktuální stream odpovídá některému z klíčových slov tohoto objektu.
        /// Nalezené klíčové slovo je předáno v parametru.
        /// V této chvíli, když je volána tato metoda, je již nastavena property this.IsUsed na true.
        /// Tato metoda v bázové třídě ze streamu odebere klíčové slovo (všechny jeho prvky), takže ve streamu jsou na řadě následující hodnoty.
        /// Toto přetížení (se dvěma parametry) nikam neukládá reálně přečtená slova.
        /// Existuje přetížení s out parametrem values, které ukládá reálně nalezené položky do výstupu, aby si je volající mohl zpracovat (typicky vyhledá slova odpovídající wildcards "?").
        /// </summary>
        /// <param name="stream">SqlStream</param>
        /// <param name="keyword">Klíčové slovo, které bylo reálně nalezeno</param>
        protected virtual void Load(SqlStream stream, string keyword)
        {
            stream.SkipKeyword(keyword);
        }
        /// <summary>
        /// Metoda je volána tehdy, když bylo zjištěno, že aktuální stream odpovídá některému z klíčových slov tohoto objektu.
        /// Nalezené klíčové slovo je předáno v parametru.
        /// V této chvíli, když je volána tato metoda, je již nastavena property this.IsUsed na true.
        /// Tato metoda v bázové třídě ze streamu odebere klíčové slovo (všechny jeho prvky), takže ve streamu jsou na řadě následující hodnoty.
        /// Toto přetížení s out parametrem values ukládá reálně nalezené položky do výstupu, aby si je volající mohl zpracovat (typicky vyhledá slova odpovídající wildcards "?").
        /// </summary>
        /// <param name="stream">SqlStream</param>
        /// <param name="keyword">Klíčové slovo, které bylo reálně nalezeno</param>
        protected virtual void Load(SqlStream stream, string keyword, out List<ParserSegmentValue> values)
        {
            stream.SkipKeyword(keyword, out values);
        }
        #endregion
        #region Detekce klíčových slov IsMySqlPart()
        /// <summary>
        /// Metoda má zjistit, zda stream aktuálně ukazuje na slovo,
        /// kterým začíná zdejší část SQL příkazu.
        /// Bázová metoda ve třídě SqlPart získá seznam klíčových slov z property this.KeyWords,
        /// a vrátí true pokud aktuální stream začíná některým z nich.
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        protected virtual bool IsMySqlPart(SqlStream stream)
        {
            return stream.StartsWithKeyword(this.Keywords);
        }
        ///// <summary>
        ///// Metoda v daném streamu najde a vrátí první z daných klíčových slov nebo sousloví.
        ///// Vstupní stream posouvá.
        ///// Zadaná slova mohou být víceslovná, každá jedna položka soupisu keyWords 
        ///// se rozdělí v místě mezer na jednotlivá slova, a ta se ve streamu detekují.
        ///// Pozor tedy na pořadí položek v soupisu keyWords: pokud budeme mít v první položce seznamu jedno slovo, 
        ///// a v druhé položce seznamu sousloví začínající předchozím slovem, pak se vždy najde první text.
        ///// Nehledá se tedy nejdelší shodný text, ale první vyhovující.
        ///// </summary>
        ///// <param name="stream"></param>
        ///// <param name="keyWords"></param>
        ///// <returns></returns>
        //protected string FindKeyWordInStream(SqlStream stream, string keyWords)
        //{
        //    if (keyWords == null) return null;
        //    string[] keyws = keyWords.Split(';', ',');
        //    List<ParserSegmentValue> streamValues = new List<ParserSegmentValue>();
        //    // Zde budu střádat aktuální slova přicházející ze streamu, abych nemusel pro každé slovo z KeyWord 
        //    // resetovat stream a načítat je znovu...  (na pozici, kde nebude Text, bude v streamWords hodnota null).
        //    foreach (string keyWord in keyws)
        //    {
        //        if (String.IsNullOrEmpty(keyWord)) continue;
        //        // Jeden keyWord může znít například: "LEFT OUTER JOIN", zde jej rozkrájím na tři slova (ve streamu budou odděleně, dokonce mezi nimi může být i komentář):
        //        string[] keys = keyWord.Split(' ');
        //        // Do své paměti streamWords načtu potřebný počet vstupujících slov:
        //        if (this.FillStreamValuesToCount(streamValues, stream, keys.Length))
        //        {
        //            if (this.StreamVaulesContainWord(streamValues, keys))
        //                return keyWord;
        //        }
        //    }
        //    return null;
        //}
        ///// <summary>
        ///// Zajistí, že paměť hodnot streamValues bude doplněna ze streamu na daný počet hodnot.
        ///// Vrací true, pokud se podaří naplnit, nebo false když ve streamu není potřebný počet hodnot.
        ///// </summary>
        ///// <param name="streamValues"></param>
        ///// <param name="stream"></param>
        ///// <param name="count"></param>
        ///// <returns></returns>
        //protected bool FillStreamValuesToCount(List<ParserSegmentValue> streamValues, SqlStream stream, int count)
        //{
        //    while (streamValues.Count < count)
        //    {
        //        ParserSegmentValue value = stream.ReadValue();
        //        if (value == null) return false;
        //        streamValues.Add(value);
        //    }
        //    return streamValues.Count >= count;
        //}
        ///// <summary>
        ///// Zjistí, zda paměť hodnot streamValues obsahuje identická slova, jaká jsou zadaná v soupisu "keys".
        ///// Konkrétně: na stejné pozici v poli streamValues musí být prvek obsahující text, který je uveden v poli keys (anebo pokud je v keys uvedena hvězdička, pak v streamValues může být cokoliv).
        ///// Pouze pak vrací true.
        ///// Poznámka: v poli streamValues může být více prvků než v poli keys, nadpočetné prvky budou ignorovány.
        ///// Pokud bude v poli streamValues méně prvků než v keys, vrátí se false.
        ///// </summary>
        ///// <param name="streamValues"></param>
        ///// <param name="keys"></param>
        ///// <returns></returns>
        //protected bool StreamVaulesContainWord(List<ParserSegmentValue> streamValues, string[] keys)
        //{
        //    if (streamValues == null || keys == null) return false;
        //    if (streamValues.Count < keys.Length) return false;
        //    for (int i = 0; i < keys.Length; i++)
        //    {
        //        if (keys[i] != "?" && !streamValues[i].ContainText(keys[i]))
        //            return false;
        //    }
        //    return true;
        //}
        #endregion
        #region Implementace ISqlPart
        /// <summary>
        /// Metoda zjistí, zda ve streamu je jako první (platné) slovo to slovo, které je klíčem k této třídě.
        /// Například klíčové slovo pro příkaz SELECT je slovo "SELECT", pro frázi FROM je to slovo "FROM",
        /// pro frázi JOIN to může být "LEFT", "RIGHT", "OUTER", "INNER", "JOIN", 
        /// pro frázi ORDER BY musí být po sobě dvě slova "ORDER" a "BY", a další podobná pravidla.
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public bool IsMyPart(SqlStream stream)
        {
            using (stream.Preview())
            {   // Nakoukneme do streamu, jaké je první významné slovo v příkazu:
                return this.IsMySqlPart(stream);
            }
        }
        /// <summary>
        /// Metoda zajistí vyvolání načtení dat ze streamu přímo = bez testů Keywords.
        /// Označí objekt jako IsUsed = true a vyvolá protected metodu Load s parametrem keyWord = null.
        /// </summary>
        /// <param name="stream"></param>
        public void LoadDirect(SqlStream stream)
        {
            this.IsUsed = true;
            this.StreamInit(stream);
            this.Load(stream, null);
            this.StreamDone(stream);
        }
        /// <summary>
        /// Metoda může být volaná kdykoliv, za jakéhokoli stavu streamu - tedy i když stream neobsahuje data pro tuto parts.
        /// Objekt pak nesmí stream posunout, ale musí v klidu skončit (ani chybu nemá vyhodit).
        /// Tato technika je potřebná pro sekvenční načítání vnitřních částí SQL příkazu, kdy SqlCommand obsahuje sekvenci příkazů Part.LoadForm() pro jednotlivé své SqlParts.
        /// Pořadí částí v SQL příkazu je povinné, proto je možno v pevně daném pořadí načítat jednotlivé Parts.
        /// Některé Parts jsou nepovinné, takže mají zůstat prázdné.
        /// Tato metoda v takovém případě uloží do objektu příznak "Not used part".
        /// Objekt do sebe načte data přicházející ze streamu, stream se tím posouvá.
        /// Jakmile objekt narazí na část streamu, kterou nezná, skončí.
        /// </summary>
        /// <param name="stream"></param>
        public void LoadFrom(SqlStream stream)
        {
            this.StreamInit(stream);

            this.Keyword = null;
            using (stream.Preview())
            {   // Nakoukneme do streamu, zda obsahuje některé naše klíčové slovo, a případně které to je:
                this.Keyword = stream.FindInitialKeyword(this.Keywords);
            }

            this.IsUsed = (this.Keyword != null);

            if (this.IsUsed)
            {
                this.Keyword = this.Keyword;
                this.Load(stream, this.Keyword);
            }

            this.StreamDone(stream);
        }
        /// <summary>
        /// Zaznamená do sebe referenci na stream a jeho pozici při počátku načítání dat ze streamu.
        /// Je voláno v LoadFrom() a LoadDirect().
        /// </summary>
        /// <param name="stream"></param>
        protected void StreamInit(SqlStream stream)
        {
            this.Stream = stream;
            this.StreamPointer = stream.Pointer;
            this.StreamLength = 0;
        }
        /// <summary>
        /// Zaznamená do sebe referenci na stream a jeho pozici při konci načítání dat ze streamu.
        /// Je voláno v LoadFrom() a LoadDirect().
        /// </summary>
        /// <param name="stream"></param>
        protected void StreamDone(SqlStream stream)
        {
            this.StreamLength = stream.Pointer - this.StreamPointer;
        }
        #endregion
    }
    #endregion
    #region class KeywordPhrase : sousloví a jeho slova a metody (CompareByCountDesc(), IsEquivalent())
    /// <summary>
    /// KeywordPhrase : sousloví a jeho slova a metody (CompareByCountDesc(), IsEquivalent())
    /// </summary>
    public class KeywordPhrase
    {
        /// <summary>
        /// Konstruktor. 
        /// Na vstupu se očekává text například "ORDER BY", "LEFT OUTER ? JOIN", "WHERE", atd.
        /// </summary>
        /// <param name="phrase"></param>
        public KeywordPhrase(string phrase)
        {
            this._Fill(phrase, new char[] { ' ' });
        }
        /// <summary>
        /// Konstruktor. 
        /// Na vstupu se očekává text například "ORDER BY", "LEFT OUTER ? JOIN", "WHERE", atd; a sada separátorů (defaultní je mezera).
        /// </summary>
        /// <param name="phrase"></param>
        /// <param name="separators"></param>
        public KeywordPhrase(string phrase, char[] separators)
        {
            this._Fill(phrase, separators);
        }
        private void _Fill(string phrase, char[] separators)
        {
            string p = phrase.Trim();
            this.Phrase = p;
            string[] words = p.Split(separators, StringSplitOptions.RemoveEmptyEntries);
            this.WordCount = words.Length;
            this.Words = new string[this.WordCount];
            for (int w = 0; w < this.WordCount; w++)
                this.Words[w] = words[w].Trim();
        }
        /// <summary>
        /// Vstupní text fráze
        /// </summary>
        public string Phrase { get; private set; }
        /// <summary>
        /// Fráze separovaná na slova
        /// </summary>
        public string[] Words { get; private set; }
        /// <summary>
        /// Počet slov fráze
        /// </summary>
        public int WordCount { get; private set; }
        /// <summary>
        /// Normalizovaná fráze (velkými písmeny, mezi slovy jedna mezera)
        /// </summary>
        public string PhraseNormalised
        {
            get
            {
                string result = "";
                foreach (string w in this.Words)
                    result += (result.Length == 0 ? "" : " ") + w.Trim().ToUpper();
                return result;
            }
        }
        /// <summary>
        /// Porovná dvě hodnoty podle WordCount, sestupně (pro třídění).
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static int CompareByCountDesc(KeywordPhrase a, KeywordPhrase b)
        {
            return b.WordCount.CompareTo(a.WordCount);
        }
        /// <summary>
        /// Zjistí, zda dodaný seznam hodnot ze streamu obsahuje zde definovaná slova.
        /// </summary>
        /// <param name="streamValues"></param>
        /// <returns></returns>
        internal bool IsEquivalent(List<ParserSegmentValue> streamValues)
        {
            if (this.WordCount > streamValues.Count) return false;          // streamValues je sice přednačtený na potřebný počet slov, ale fyzicky ve streamu nemusí být dostatek textu na potřebný počet - takže může být kratší.
            for (int w = 0; w < this.WordCount; w++)
            {
                ParserSegmentValue value = streamValues[w];
                if (!this.IsEquivalent(value, w))
                    return false;
            }
            return true;
        }
        /// <summary>
        /// Vrátí true, pokud daná hodnota (value) odpovídá slovu na dané pozici
        /// </summary>
        /// <param name="value"></param>
        /// <param name="wordIndex"></param>
        /// <returns></returns>
        internal bool IsEquivalent(ParserSegmentValue value, int wordIndex)
        {
            if (value == null) return false;
            string word = this.Words[wordIndex];
            if (word == "?") return true;
            if (value.ValueType == ParserSegmentValueType.Delimiter)
                return (value.Content == word);
            else
                return value.ContainText(word, true);
        }
    }
    #endregion
    #region interface ISqlCommand : definuje povinnosti pro třídy, které představují SQL příkaz
    /// <summary>
    /// Předpis pro všechny objekty, které představují SQL příkaz
    /// </summary>
    public interface ISqlCommand : IPlugin, ISqlPart
    {
        /// <summary>
        /// Název SQL příkazu
        /// </summary>
        string SqlCommand { get; }
    }
    #endregion
    #region interface ISqlPart : dvě metody, které z čehokoli udělají část SQL commandu: IsMyPart() a LoadPart()
    /// <summary>
    /// Předpis pro všechny objekty, které do sebe dokážou načíst určité části SQL příkazu
    /// </summary>
    public interface ISqlPart
    {
        /// <summary>
        /// Objekt zjistí, zda pro aktuální stream SQL položek je tento objekt ten pravý pro zpracování (detekuje úvodní klíčová slova).
        /// Stream se neposouvá, ale prvek sám může ze streamu číst (i přeskakovat komentáře) díky technice streamu using (stream.Preview()) { stream.Find...() }
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        bool IsMyPart(SqlStream stream);
        /// <summary>
        /// Metoda může být volaná kdykoliv, za jakéhokoli stavu streamu - tedy i když stream neobsahuje data pro tuto parts.
        /// Objekt pak nesmí stream posunout, ale musí v klidu skončit (ani chybu nemá vyhodit).
        /// Tato technika je potřebná pro sekvenční načítání vnitřních částí SQL příkazu, kdy SqlCommand obsahuje sekvenci příkazů Part.LoadForm() pro jednotlivé své SqlParts.
        /// Pořadí částí v SQL příkazu je povinné, proto je možno v pevně daném pořadí načítat jednotlivé Parts.
        /// Některé Parts jsou nepovinné, takže mají zůstat prázdné.
        /// Tato metoda v takovém případě uloží do objektu příznak "Not used part".
        /// Objekt do sebe načte data přicházející ze streamu, stream se tím posouvá.
        /// Jakmile objekt narazí na část streamu, kterou nezná, skončí.
        /// </summary>
        /// <param name="stream"></param>
        void LoadFrom(SqlStream stream);
    }
    #endregion
    #region Testovací třída
    public static class SqlCommandTest
    {
        public static void Run()
        {
            string sql = CreateCommand(2);
            var list = SqlCommands.SqlCommand.Parse(sql);
        }
        private static string CreateCommand(int command)
        {
            switch (command)
            {
                case 1: return Command1;
                case 2: return Command2;
                case 3: return Command3;
            }
            return "";
        }
        private static string Command1
        {
            get
            {
                return @"SELECT
p.cislo_subjektu,
p.cislo_objektu,
p.zdroj,
/* p.sklad, */
isnull(skl.sklad_mrp,skl.cislo_subjektu) sklad_mrp,
case st.povin_dim_varianta_zbozi when 'A' then p.varianta_zbozi else null end varianta_zbozi,
case st.povin_dim_obchodni_pripad when 'A' then p.obchodni_pripad else null end obchodni_pripad,
(select min(cislo_podkladu) from lcs. podklady pod where pod.cislo_subjektu = p.podklad) as cislo_podkladu,
max(pocet) / min(pocet) as pomer_maxmin,
p.typ,
p.pocet qty,
p.pocet_real qty_real,
p.datum,
/* case when (getdate() > p.datum) then getdate() else p.datum end datum, */
p.datum_plan,
/* case when (getdate() > p.datum_plan) then getdate() else p.datum_plan end datum_plan, */
/*  DAJ: následující 4 řádky jsou data pro doplňování stavu skladu, metodou stavSkladuCreateAndReFill() */
st.cislo_nonsubjektu as st_cislo_nonsubjektu,
p.varianta_zbozi as p_varianta_zbozi,
p.obchodni_pripad as p_obchodni_pripad,
'A' as refill_var_and_ord,
st.dodavka,
st.rezerva,
p.referencni_cislo reference_number,
h.sklad2 sklad2
FROM lcs.sk_polozka p
join lcs.sk_sklad skl on p.sklad=skl.cislo_subjektu
left outer join lcs.stav_sk st on st.zdroj=p.zdroj and st.sklad=isnull(skl.sklad_mrp,skl.cislo_subjektu)
join lcs.sk_hlavicka h on h.cislo_subjektu = p.cislo_subjektu
/*  DAJ: akceptovat pouze položky dokladu, kde je navázaná kmenová karta skladu, a ne obecný zdroj : */
join lcs.kmenova_karta_skladu kkzm on kkzm.cislo_subjektu = p.zdroj
WHERE p.stav = 1
/* and p.cislo_objektu = 5365 */
/*  and p.zdroj = 72467 */
and p.typ in ('3','4','5')
and (p.pocet - p.pocet_real) > 0
and p.splneno_uziv = 'N'
and skl.planovat = 'A' /* mrp sklad má plánovat vždy, zohledňuji normální sklad */";
            }
        }
        private static string Command2
        {
            get
            {
                return @"SELECT
pos.product_order cislo_subjektu,
null cislo_objektu,
pos.cislo_nonsubjektu cislo_nonsubjektu,
pos.source source,
isnull(skl.sklad_mrp,skl.cislo_subjektu) sklad_mrp,
pos.variation variation,
pos.order_relation order_relation,
'Q' typ,
pos.qty qty,
/*  DAJ: jako REALIZOVÁNO se pro MRP bere REALIZOVÁNO + REZERVOVÁNO (vývoj 0028095)   */
/*  pos.qty_real qty_real,  */
(isnull(pos.qty_real,0) + isnull(pos.qty_booked, 0)) as qty_real,
pos.date_plan date_plan,
pos.a_constr_element a_constr_element,
pos.constr_element constr_element,      /* DAJ 0029613 */
pos.plan_unit_s plan_unit_s,
pos.constr_element constr_element,
/*  DAJ: následující 1 řádek obsahuje data pro doplňování stavu skladu, metodou stavSkladuCreateAndReFill() */
st.cislo_nonsubjektu as st_cislo_nonsubjektu,
st.dodavka,
st.rezerva,
pos.status
FROM lcs.product_order_structure pos
join lcs.product_order po on po.cislo_subjektu = pos.product_order
join lcs.sk_sklad skl on pos.stock=skl.cislo_subjektu
left outer join lcs.stav_sk st on st.zdroj=pos.source and st.sklad=isnull(skl.sklad_mrp,skl.cislo_subjektu)
WHERE
skl.planovat = 'A'
/* DAJ 0026413 28.8.2007 : uvádět pouze komponenty na neuzavřených VP, s nenulovým zůstatkem, ve stavu pouze N */
and po.status IN ('N','P','J') and pos.status = 'N'
/* DAJ 0027542 21.1.2008 : množství qty_real ne zcela naplňuje požadované množství qty (obojí může být kladné i záporné) */
and (sign(pos.qty - pos.qty_real) = sign(pos.qty))
and pos.product_order in
(  /* mají nějakou nevydanou komponentu */
(select product_order
from lcs.product_order_structure pos1
where pos1.product_order = pos.product_order
and pos1.status not in ('A','J')
group by pos1.product_order
/* having sum(pos1.qty - pos1.qty_real) > 0)  */
having sum(case when (sign(pos1.qty - pos1.qty_real) = sign(pos1.qty)) then 1 else 0 end) > 0)
union
/* mají neodvedvený zůstatek na VP */
(select cislo_subjektu from lcs.product_order po1
where po1.qty_source - po1.qty_real - po1.qty_reject > 0
and po1.status IN ('N','P'))
)";
            }
        }
        private static string Command3
        {
            get
            {
                return @"
SELECT
 t1.cislo_subjektu as subjekt,
 max(t2.pocet) as maxpocet,
 (select q3.stav from table3 q3 where q3.klic = t1.klic) as q3stav
from 
 [database].[case].[where] t1
   left join tabulka2 as t2 with (readuncommitted) on t2.klic = t1.key,
 (select * from table4 q4) q4
   left outer hash join table6 t6 on t6.cislo= t1.key
where a=2 and q4.klic = t1.key
order by 1 desc
group by column1
";
            }
        }
    }
    #endregion
}
